feat(desktop): add KLIPY GIF search to composers - #5554
Conversation
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
…picker Signed-off-by: kenny lopez <klopez4212@gmail.com> # Conflicts: # desktop/src/features/custom-emoji/ui/EmojiPicker.tsx # desktop/src/features/forum/ui/ForumComposer.tsx
Signed-off-by: kenny lopez <klopez4212@gmail.com>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Thanks for putting this together — the relay proxy is genuinely well built. The key stays server-side with a redacted Debug impl, NIP-98 verification requires the payload hash, and there's a replay guard, membership + per-pubkey quota enforcement, an upstream response-size cap, and allowlisting so KLIPY error bodies never cross the relay boundary. That whole boundary held up under both source review and a live run against the real KLIPY API.
I'm requesting changes on two blocking issues, both verified end-to-end, plus one design question I'd like resolved before merge:
-
Selecting a GIF produces a message the relay refuses to ingest — the core feature doesn't work end-to-end. A live run against this branch got
400 {"error":"invalid: imeta url must be a local /media/ path"}on both stream (kind9) and forum (kind45001) sends, with zero posts persisted. Details inline atdesktop/src/features/gifs/api.ts. -
The PR breaks its own smoke test:
composer-tooltip-dismiss.spec.tsstill locates theInsert emojitooltip, and Desktop Smoke E2E (2) failed that assertion on the initial attempt and both retries. Details inline atComposerEmojiPicker.tsx. -
KLIPY's own reference integrations couple sends to their share/report endpoint, and this PR documents skipping that as a non-goal — I don't think that call is as free as the docs suggest. Inline comment on
crates/buzz-relay/src/api/gifs.rswith references.
There are also a few smaller things inline (upstream client reuse, handler-level test coverage, per-relay customer_id keying, and some nits).
Co-authored-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 All feedback from the combined review is addressed at 9db36e2 — verified each finding at source, and both blockers are confirmed fixed: external GIFs now send as content-only markdown (no imeta tag, so the relay validator is never engaged) and the tooltip smoke spec is green. The new /gifs/share endpoint sits behind the same NIP-98/membership/replay boundary as search, and live E2E against the real KLIPY provider confirmed sends persist and render on both kind 9 and forum posts, plus fully graceful degradation (no GIF tab, no errors, zero /gifs/* requests) when the relay has no KLIPY key. CI is fully green at head.
jedwards27
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Reviewed: aea0ef8df9fc24d9aa8bf5c761ab2910026a601b..9db36e28afbe76aa3575b0665baa2b316de9d90d (exact head 9db36e28afbe76aa3575b0665baa2b316de9d90d)
Risk: high — this adds a credentialed relay-to-provider HTTP boundary plus externally hosted animated media in shared message/forum composer paths.
Behavior/contracts traced: relay config and NIP-11 capability advertisement; NIP-98/membership/replay/rate-limit admission; KLIPY search/share URL construction and response limits; renderer capability discovery and provider requests; GIF normalization/selection; content-only attachment serialization; shared composer preview/lightbox; forum/message composer reuse; loading/error/empty, motion, and accessibility paths.
Blocking findings
-
[P1] Provider redirects create an SSRF primitive and can disclose the path-embedded API key.
crates/buzz-relay/src/state.rs:858-861builds the shared reqwest client without a redirect policy. Reqwest follows redirects by default and enablesReferer; meanwhilecrates/buzz-relay/src/api/gifs.rs:75-105,281-282,317-321placesBUZZ_KLIPY_API_KEYin the request path for both GET search and POST share. A KLIPY 3xx can therefore make the relay connect to an arbitrary redirect target, including loopback/private/link-local services. A cross-originReferercan carry the prior full key-bearing URL, and 307/308 can replay the share POST body.Author action: configure the dedicated GIF client with
reqwest::redirect::Policy::none()and treat 3xx as provider failure. Add a causal regression with a redirecting fake upstream and a second listener: assert the second listener receives zero requests and the client-facing error contains neither the API key nor redirect target. Mutation-check the test by restoring default redirects and requiring it to fail.Verification owner: author for fix/regression; reviewer for mutation check.
-
[P1] Reduced-motion users still receive an autoplaying grid of provider animations.
desktop/src/features/gifs/ui/KlipyGifPicker.tsx:108-126renders every preview URL as an ordinary image. The only reduced-motion branch is for the 180 ms tab indicator (desktop/src/features/messages/ui/ComposerEmojiPicker.tsx:44,128-153); there is no static poster, paused state, activation gate, or stop/hide control for the feature's dominant repeating motion. One search can therefore start many indefinite animations simultaneously despite the PR's stated reduced-motion behavior.Author action: under
prefers-reduced-motion: reduce, render a non-animated poster when the provider supplies one, or a static named placeholder requiring explicit activation before loading animated media. Add deterministic Playwright coverage usingpage.emulateMedia({ reducedMotion: "reduce" })proving animated URLs are not loaded/rendered before activation, and mutation-prove the regression.Verification owner: author for code/regression; reviewer for exact-head browser/AX inspection.
-
[P2] GIF selection discards its accessible name in the composer preview and lightbox. The picker has the useful title (
KlipyGifPicker.tsx:110-125), butklipyGifAttachment()does not preserve it and sets an empty hash (desktop/src/features/gifs/api.ts:160-169). Shared attachment UI derives all image names from that hash (desktop/src/features/messages/ui/ComposerAttachments.tsx:267,364-368,393-399,427-434), producing indistinguishableAttachment/Attachment previewnames for every selected KLIPY GIF.Author action: preserve the normalized GIF title on the composer attachment (the existing
displayLabelfield is suitable), and have the shared attachment preview/lightbox use a non-empty descriptive label while hiding decorative duplicates. Add coverage selecting at least two differently titled GIFs and asserting distinct accessible names in both preview and lightbox.Verification owner: author for regression; reviewer for AX-tree/browser verification.
Validation
- PASS — full Desktop package tests:
pnpm test, 5,312 passed / 0 failed, exact head and clean tree rechecked. - PASS —
git diff --check aea0ef8df9fc24d9aa8bf5c761ab2910026a601b..HEAD. - PASS — all required exact-head GitHub checks are green, including Unit Tests, Desktop Core, Desktop smoke/integration, relay integration/E2E, security, cross-compile, and macOS build.
- ENVIRONMENT FAILURE —
cargo test -p buzz-relay: 894 passed, 8 failed, 47 ignored; all eight failures were database-unavailable shapes (PoolTimedOutor dependent status mismatches). Exact-head Unit Tests and relay integration/E2E checks are green, so this is not attributed to the PR. - REVIEWER-LOCAL CONFIDENCE GAP — a focused smoke row was 1 pass / 1 fail twice while reusing a port-4173 server; the positive capability row did not observe mocked
/info. This conflicts with green exact-head CI and is not classified as another author defect.
Manual/native evidence: no new exact-head native Desktop/AX journey was completed in this review. Source inspection establishes the three defects above independently.
Residual risk: configured-route auth/replay/membership/quota rejection is not directly covered at the handler boundary; source composes the established shared gates correctly. Native keyboard/AX, dark theme, narrow-window, and live forum-send journeys remain unwitnessed. These are confidence gaps, not additional author actions.
— :bot: Jude’s code review agent
…n, a11y names Resolve the three blocking findings on PR #5554: - P1 SSRF/key disclosure: the dedicated KLIPY reqwest client followed redirects by default while the API key rides in the request path, so a provider 3xx could replay a key-bearing URL to an attacker-chosen host. build_gif_http_client() now sets redirect::Policy::none(); a 3xx returns as a non-success status the handlers map to a generic 502, never reading or forwarding Location. Adds a causal regression with a redirecting fake upstream and a second listener asserting zero hits and no leak. - P1 reduced motion: the picker grid autoplayed every animated preview. Under prefers-reduced-motion: reduce it now renders a static provider poster (a normalized jpg asset) or a named static placeholder, and reacts to preference changes while mounted. - P2 accessibility: KLIPY GIFs dropped their title, leaving empty 'Attachment ' names in the composer preview and lightbox. displayLabel now carries the title through BlobDescriptor; MediaAttachmentItem derives one non-empty accessible name for the thumbnail, dialog, editor, lightbox, and remove control. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker * origin/main: (57 commits) Add staging dev relay image workflow (#6709) Extract community persistence (#6668) Fix mobile Huddle agent voice turn states (#6611) Add inline profile camera capture (#6680) Hide Huddles in mobile agent DMs (#6676) fix(desktop): polish inline chip states (#6718) Centralize replaceable event persistence (#6660) feat(workflows): discover trigger filter values (#6712) feat(desktop): simplify the message action rail (#6529) fix(desktop): restore icon-only remote marker (#6491) fix(ci): prevent poisoned Rust caches (#6618) docs(security): route reports through private advisories (#6728) fix(composer): wrap Buzz chip labels without orphaning icons (#6581) fix(desktop): bound thread /query and surface load errors, not false-empty (#6447) fix(messages): route edits to the owning composer (#6575) fix(mobile): join starter channels after accepting invite (#5915) Add mobile profile editing (#6583) fix(desktop): align jump-to-latest pill with composer height (#6606) fix(desktop): emit singular `mention` feed category so alerts route correctly (#6665) fix(mobile): recover stale and shuffled messages (#6691) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The desktop file-size ratchet freezes tauri.ts and MessageComposer.tsx, both already over the 1000-line ceiling on main. Carry the KLIPY GIF title on the existing ImetaMedia type (which already declares the composer-only displayLabel) instead of widening the frozen BlobDescriptor, and widen MediaAttachmentItem's prop to ImetaMedia to read it. Collapse one useMentions call to a single line so the merge's essential gifMediaController wiring keeps MessageComposer.tsx at its frozen size. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The accessible-name fallback preferred filename over the historical hash label, so ordinary image/video uploads (which carry a filename) lost their 'Attachment <hash>' / 'Video attachment <hash>' names — breaking the exact assertions in composer-image-draw, video-attachment, and spoiler specs. Prefer displayLabel, then the type-aware hash label when sha256 is present, and only fall back to filename for genuinely hashless non-provider media. Make the lightbox view image decorative since the dialog title already names it, removing the duplicate accessible name. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker * origin/main: highlight search terms in results and messages (#6702) fix(desktop): make lightbox zoom controls interactive (#6710) Support community deletion in versioned media buckets (#6738) Fix TipTap editor mount race (#6779) feat(buzz-agent): gate LLM tool calls on session/request_permission (#5712) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Radix Trigger with asChild on a bare div left the interactive element
role-less, so the composer attachment had no reliable single accessible
name (and the video-without-poster branch had none at all). Render the
trigger as a real button carrying aria-label={mediaLabel}, make both
thumbnail images decorative, and locate the trigger by role/name in the
composer, spoiler, video, and GIF specs.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
openGifGrid() already opens the emoji/GIF popover, but the selection loop's first iteration re-clicked composer-emoji-button — the Radix PopoverTrigger — toggling the just-opened popover closed. The GIFs-tab click then raced the popover's exit/unmount, so Playwright saw the target go unstable then detach and timed out (deterministic across all retries). The popper position itself never moved. Drop the redundant openGifGrid() preamble so each loop iteration owns the single open→select cycle it expects. The other three picker tests keep using openGifGrid() unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker * origin/main: Qualify canonical relay images for staged delivery (#6781) feat(desktop): persist agent addressing across composer messages (#6714) feat: navigate images across message threads (#6705) Add database pressure observability (#6700) revert fixed mention highlight (#6716) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> # Conflicts: # desktop/src/features/messages/ui/MessageComposer.tsx
Main #6793 grew MessageComposer.tsx to the 1000-line ceiling; the branch's gifMediaController prop tipped it one line over the ratchet. Drop the redundant trackDraftAuthoredContent destructure alias (single call site) to reclaim the line without touching the ratchet config or behavior. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker * origin/main: Remove public relay signing key fallback (#6729) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The error toast raised when agent startup fails renders bottom-right, overlapping the send button. The first send leaves the pointer parked on the toast, which pauses sonner's auto-dismiss timer, so the retry click is intercepted for the full timeout. Move the cursor off the toast and wait for it to clear before retrying. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker * origin/main: chore(release): release Buzz Desktop version 0.5.19 (#6828) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…-history * origin/main: feat(desktop): add KLIPY GIF search to composers (#5554) fix(desktop): respect automatic mention preference after send (#6837) fix(release): attribute desktop candidates to the operator (#6831) fix(ci): check out source in docker.yml merge job (#6833) chore(release): release Buzz Desktop version 0.5.19 (#6828) Remove public relay signing key fallback (#6729) docs(nest): make commit attribution policy-neutral (#6707) fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…cp-sessions * origin/main: (31 commits) fix(desktop): stop pulsing addressed agents on send (#6873) fix(desktop): prioritize sidebar channel status (#6861) feat(desktop): hyperlink selected composer text on link paste (#6684) chore(release): release Buzz Desktop version 0.5.20 (#6839) feat(desktop): add KLIPY GIF search to composers (#5554) fix(desktop): respect automatic mention preference after send (#6837) fix(release): attribute desktop candidates to the operator (#6831) fix(ci): check out source in docker.yml merge job (#6833) chore(release): release Buzz Desktop version 0.5.19 (#6828) Remove public relay signing key fallback (#6729) docs(nest): make commit attribution policy-neutral (#6707) fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793) Qualify canonical relay images for staged delivery (#6781) feat(desktop): persist agent addressing across composer messages (#6714) feat: navigate images across message threads (#6705) Add database pressure observability (#6700) revert fixed mention highlight (#6716) highlight search terms in results and messages (#6702) fix(desktop): make lightbox zoom controls interactive (#6710) Support community deletion in versioned media buckets (#6738) ... Signed-off-by: Salman Mohammed <smohammed@squareup.com>
* origin/main: Use paired tags for standing & per-turn context (#6701) fix(cli): preserve signatures in event reads (#6884) refactor(db): finish replaceable event store extraction (#6777) Fix Admin feedback filter overflow (#6825) fix(desktop): stop pulsing addressed agents on send (#6873) fix(desktop): prioritize sidebar channel status (#6861) feat(desktop): hyperlink selected composer text on link paste (#6684) chore(release): release Buzz Desktop version 0.5.20 (#6839) feat(desktop): add KLIPY GIF search to composers (#5554) fix(desktop): respect automatic mention preference after send (#6837) fix(release): attribute desktop candidates to the operator (#6831) fix(ci): check out source in docker.yml merge job (#6833) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…ignment * origin/main: (33 commits) feat(desktop): restore message quick reactions (#6892) Use paired tags for standing & per-turn context (#6701) fix(cli): preserve signatures in event reads (#6884) refactor(db): finish replaceable event store extraction (#6777) Fix Admin feedback filter overflow (#6825) fix(desktop): stop pulsing addressed agents on send (#6873) fix(desktop): prioritize sidebar channel status (#6861) feat(desktop): hyperlink selected composer text on link paste (#6684) chore(release): release Buzz Desktop version 0.5.20 (#6839) feat(desktop): add KLIPY GIF search to composers (#5554) fix(desktop): respect automatic mention preference after send (#6837) fix(release): attribute desktop candidates to the operator (#6831) fix(ci): check out source in docker.yml merge job (#6833) chore(release): release Buzz Desktop version 0.5.19 (#6828) Remove public relay signing key fallback (#6729) docs(nest): make commit attribution policy-neutral (#6707) fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793) Qualify canonical relay images for staged delivery (#6781) feat(desktop): persist agent addressing across composer messages (#6714) feat: navigate images across message threads (#6705) ... Signed-off-by: Tom Brow <tomb@block.xyz> # Conflicts: # .env.example # crates/buzz-db/src/push.rs # mobile/lib/shared/profile/user_cache_provider.dart
Summary
/media/entries).Relay-to-provider boundary
BUZZ_KLIPY_API_KEYnever reaches the desktop; the key stays server-side behind a redactedDebugimpl.reqwestclient setsredirect::Policy::none(). Because the API key rides in the request path, following a provider3xxcould replay a key-bearing URL to an attacker-chosen host (an SSRF/key-disclosure primitive). With redirects disabled, a3xxreturns as a non-success status that the handlers map to a generic502; theLocationtarget is never read or forwarded.Accessibility
prefers-reduced-motion: reduce, the picker grid renders a static provider poster (a normalizedjpgasset) instead of the animated preview, or a named static placeholder when no poster is available. It reacts to preference changes while mounted.no-preferencekeeps the animated preview.ImetaMedia'sdisplayLabel, so the composer thumbnail, preview dialog, editor, lightbox, and remove control all derive one non-empty accessible name instead of an emptyAttachmentlabel. Ordinary hashed uploads keep their existing hash-derived names.